<!DOCTYPE html>
<html class="rootScroller">
<!-- This file is based on cross_site_iframe_factory.html. See comments in that
file as well as tree_parser_util.js for more details and syntax information.

This page creates a nested, non-branching, frame tree with each child frame being
fully outside of its embedder's initial viewport. The inner-most frame will
create a <input> box, also outside the frame's initial viewport.

See window.Attributes map for supported attributes.

Usage Example:

  cross_site_scroll_into_view_factory
      .html?siteA{MobileViewport,RTL}(siteB(siteC{TouchActionNone}))

 -->
<head>
<title>Cross-site scroll-into-view frame tree factory</title>
<script>
  window.Attributes = {
    // Make's a frame's document "dir=rtl" to support testing right-to-left
    // writing modes.
    RTL: 0,
    // Adds a viewport meta tag to a frame, making it "mobile friendly". This
    // occurs for subframes as well but only has an effect in the root frame.
    MobileViewport: 1,
    // Adds a viewport meta tag to a frame, making it
    // "mobile friendly" and also limiting minimum-zoom. This occurs for
    // subframes as well but only has an effect in the root frame.
    MobileViewportNoZoom: 2,
    // Adds a `touch-action: none` style to the inner-most
    // frame's <input> box. No-op in other frames.
    TouchActionNone: 3,
    // Uses a <fencedframe> element rather than an <iframe> for the child
    // frame.
    FencedFrame: 4,
    // Puts the <input> box into a root scroller element.
    RootScroller: 5,

    // Keep this up to date with last enum.
    MAX_VALUE: 5
  }
</script>
<style>
  html,body {
    width: 100%;
    height: 100%;
  }
  html {
    overflow: hidden;
  }
  .rootScroller {
    overflow: auto;
  }
  iframe, fencedframe {
    position: absolute;
    /* 4 screens worth of inset, to make sure this is off screen on Android
     * where minimum scale on load is 0.25 */
    inset-inline-start: 400%;
    inset-block-start: 400%;
    width: 60%;
    height: 60%;
  }
  input {
    position: absolute;
    inset-inline-start: 400%;
    inset-block-start: 400%;
    width: 50%;
  }
  div.rootScroller {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    background-color: coral;
  }
  .spacer {
    /* make the document scrollable */
    position: absolute;
    inset-inline-start: 0;
    inset-block-start: 0;
    width: 1200%;
    height: 1200%;
  }

  .touchActionNone {
    touch-action: none;
  }

  .layoutViewport {
    /* Used to let the test measure the size of the layout viewport; needed when
     * testing desktop page on mobile where ICB size doesn't match layout
     * viewport size (i.e. the "shrinks viewport contents to fit" setting) */
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    visibility: hidden;
  }
</style>
</head>
<body>
<div class="layoutViewport"></div>
<h2 id='siteNameHeading'></h2>
<div class="spacer"></div>


<script src='tree_parser_util.js'></script>
<script>

function backgroundColorForSite(site) {
  var lightness = 0.75;

  // The site names will be of the form siteA, siteB, etc so map the fifth
  // character to an index. This could be negative, we don't really care.
  var index = site.charCodeAt(4) - 'a'.charCodeAt(0);

  // If the first character is 'a', this will the the starting color.
  var hueOfA = 200;  // Spoiler alert: it's blue.

  // Color palette generation articles suggest that spinning the hue wheel by
  // the golden ratio yields a magically nice color distribution. Something
  // about sunflower seeds. I am skeptical of the rigor of that claim (probably
  // any irrational number at a slight offset from 2/3 would do) but it does
  // look pretty.
  var phi = 2 / (1 + Math.pow(5, .5));
  var hue = Math.round((360 * index * phi + hueOfA) % 360);
  return 'hsl(' + hue + ', 60%, ' + Math.round(100 * lightness) + '%)';
}

function isUrl(siteString) {
  try {
    var url = new URL(siteString);
  } catch (e) {
    if (e instanceof TypeError)
      return false;
  }
  return true;
}

/**
 * Extract the specified port number, if any. Returns empty string if not
 * specified.
 */
function sitePortNumber(siteString) {
  let index = siteString.indexOf(':');
  if (index == -1)
    return ""
  return siteString.substring(index + 1);
}

/**
 * Adds ".test" to an argument if it doesn't already have a top level domain.
 * Converts "siteA" to "a.test" to match test host names defined in test SSL
 * certificate.  Adds the specified port number, if any, or the default port
 * otherwise.
 */
function canonicalizeSiteAndPort(siteString, defaultPort) {
  var portNumber = sitePortNumber(siteString) || defaultPort;
  var hostName = siteString.split(':')[0];
  if (hostName !== "localhost" && hostName.indexOf('.') == -1)
    hostName = hostName + '.test';

  if (hostName.startsWith('site'))
    hostName = hostName.substring('site'.length).toLowerCase();

  return hostName + (portNumber ? ':' + portNumber : "");
}

function hasAttribute(attribute) {
  if (!Number.isInteger(attribute) || attribute < 0
      || attribute > Attributes.MAX_VALUE) {
    throw new Error("hasAttribute parameter invalid: " + attribute);
  }

  for (var frame_attribute of frame_attributes) {
    if (frame_attribute == attribute) {
      return true;
    }
  }

  return false;
}

function processFrameAttributes(frameTree) {
  window.frame_attributes = [];
  for (var attribute of frameTree.attributes) {
    if (!Attributes.hasOwnProperty(attribute))
      throw new Error("Invalid specified attribute: " + attribute);

    frame_attributes.push(Attributes[attribute]);
  }
}

function main() {
  var goCrossSite = !window.location.protocol.startsWith('file');
  var queryString = decodeURIComponent(window.location.search.substring(1));
  var frameTree = TreeParserUtil.parse(queryString);
  var currentSite = isUrl(frameTree.value)
                      ? frameTree.value
                      : canonicalizeSiteAndPort(frameTree.value, "");

  processFrameAttributes(frameTree);

  if (hasAttribute(Attributes.RTL)) {
    document.documentElement.setAttribute('dir', 'rtl')
  }

  if (hasAttribute(Attributes.MobileViewport)) {
    const meta = document.createElement('meta');
    meta.setAttribute('name', 'viewport');
    meta.setAttribute('content', 'width=device-width');
    document.head.appendChild(meta);
  }

  if (hasAttribute(Attributes.MobileViewportNoZoom)) {
    const meta = document.createElement('meta');
    meta.setAttribute('name', 'viewport');
    meta.setAttribute('content', 'width=device-width,minimum-scale=1');
    document.head.appendChild(meta);
  }

  if (hasAttribute(Attributes.RootScroller)) {
    const html = document.documentElement;
    const kMaxOffset = 1000000;
    html.scrollLeft = kMaxOffset;
    html.scrollTop = kMaxOffset;

    html.classList.remove('rootScroller');

    const rootScroller = document.createElement('div');
    rootScroller.classList.add('rootScroller');

    const spacer = document.createElement('div');
    spacer.classList.add('spacer');
    spacer.innerText = "Root Scroller";
    rootScroller.appendChild(spacer);

    document.body.appendChild(rootScroller);
  }

  document.getElementById('siteNameHeading').appendChild(
      document.createTextNode(currentSite));

  // Apply style to the current document.
  document.body.style.backgroundColor = backgroundColorForSite(currentSite);

  if (frameTree.children.length == 0) {
    let input = document.createElement('input');

    if (hasAttribute(Attributes.TouchActionNone))
      input.classList.add('touchActionNone');

    if (document.documentElement.classList.contains('rootScroller'))
      document.body.appendChild(input);
    else
      document.querySelector('.rootScroller').appendChild(input);

    return;
  }

  // ScrollIntoView tests don't need multiple children and we don't support it.
  console.assert(frameTree.children.length == 1);

  // Compute the URL for this child frame.
  let url = frameTree.children[0].value;
  let siteAndPort = url;
  if (!isUrl(url)) {
    siteAndPort = canonicalizeSiteAndPort(url, window.location.port);
    const subtreeString = TreeParserUtil.flatten(frameTree.children[0]);
    url = '';
    url += window.location.protocol + '//';              // scheme (preserved)
    url += goCrossSite ? siteAndPort : window.location.host;    // host and port
    url += window.location.pathname;                     // path (preserved)
    url += '?' + encodeURIComponent(subtreeString);      // query
  }

  // Construct the child frame.
  var elementType = hasAttribute(Attributes.FencedFrame) ? 'fencedframe' : 'iframe';
  var frame = document.createElement(elementType);
  if (elementType == "fencedframe") {
    frame.config = new FencedFrameConfig(url);
  } else {
    frame.src = url;
  }
  frame.id = "childframe";

  // Add the child frame to the document.
  document.body.appendChild(frame);
}

main();
</script>
</body></html>
